import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import socket
import threading
import time
import queue
from ping3 import ping
from datetime import datetime
import logging
import os
from logging.handlers import TimedRotatingFileHandler
import csv


LOG_ROOT = "log"
IMPORT_ERROR_LOG = "import_error.log"

# 日志配置函数
def setup_logging():
    # 创建日志目录
    log_dir = LOG_ROOT
    os.makedirs(log_dir, exist_ok=True)

    # 主日志配置
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)

    # 日志格式
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    # 文件处理器（按天滚动）
    file_handler = TimedRotatingFileHandler(
        filename=os.path.join(log_dir, 'network_monitor.log'),
        when='midnight',
        interval=1,
        backupCount=7,
        encoding='utf-8'
    )
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)

    # 控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    console_handler.setLevel(logging.DEBUG)

    # 添加处理器
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

class NetworkMonitorApp:
    def __init__(self, root):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.logger.info("应用程序初始化开始")
        self.root = root
        self.root.title("网络检测工具")

         # 初始化线程锁 
        self.lock = threading.Lock()
        self.csv_lock = threading.Lock()  # 新增CSV写入锁

        
        # 数据存储
        self.targets = []
        self.results = {}  # {(target, type, port): {success:0, total:0, delay:0}}
        self.queue = queue.Queue()
        self.stop_event = threading.Event()

        # 创建界面组件
        self.create_widgets()
        style = ttk.Style()
        style.map('Treeview', 
                foreground=[('selected', 'black')],  # 选中项字体颜色保持黑色
                background=[('selected', '#f0f0f0')]) # 选中项背景色
        
        self.init_csv_file()  # 初始化CSV文件
        
        # 启动队列处理
        self.process_queue()
        self.logger.info("应用程序初始化完成")
        

    def create_widgets(self):
        # 主容器
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.grid(row=0, column=0, sticky="nsew")
        
        # 配置行列权重
        self.root.rowconfigure(0, weight=1)
        self.root.columnconfigure(0, weight=1)
        main_frame.columnconfigure(0, weight=1)
        
        # ========== 目标配置区域 ==========
        config_frame = ttk.LabelFrame(main_frame, text="目标配置", padding=10)
        config_frame.grid(row=0, column=0, sticky="ew", pady=5)
        config_frame.columnconfigure(1, weight=1)
        
        # 输入行
        ttk.Label(config_frame, text="目标地址:").grid(row=0, column=0, padx=5, sticky="e")
        self.target_entry = ttk.Entry(config_frame, width=25)
        self.target_entry.grid(row=0, column=1, padx=5, sticky="ew")
        
        ttk.Label(config_frame, text="类型:").grid(row=0, column=2, padx=5, sticky="e")
        self.type_var = tk.StringVar()
        self.type_combobox = ttk.Combobox(config_frame, textvariable=self.type_var, 
                                        values=["PING", "TCP", "UDP"], width=8)
        self.type_combobox.current(0)
        self.type_combobox.grid(row=0, column=3, padx=5)
        self.type_combobox.bind("<<ComboboxSelected>>", self.update_port_state)
        
        ttk.Label(config_frame, text="端口:").grid(row=0, column=4, padx=5, sticky="e")
        self.port_entry = ttk.Entry(config_frame, width=8)
        self.port_entry.grid(row=0, column=5, padx=5)
        self.update_port_state()
        
        # ========== 操作按钮区域 ==========
        action_frame = ttk.Frame(main_frame)
        action_frame.grid(row=1, column=0, pady=5, sticky="ew")
        
        ttk.Button(action_frame, text="添加目标", command=self.add_target, width=10).pack(side="left", padx=2)
        ttk.Button(action_frame, text="批量导入", command=self.import_targets, width=10).pack(side="left", padx=2)
        ttk.Button(action_frame, text="删除选中", command=self.delete_selected_targets, width=10).pack(side="left", padx=2)
        ttk.Button(action_frame, text="清空全部", command=self.delete_all_targets, width=10).pack(side="left", padx=2)
        
        # ========== 结果展示区域 ==========
        result_frame = ttk.LabelFrame(main_frame, text="检测结果", padding=10)
        result_frame.grid(row=2, column=0, sticky="nsew", pady=5)
        result_frame.rowconfigure(0, weight=1)
        result_frame.columnconfigure(0, weight=1)
        
        # 带滚动条的表格
        columns = ("target", "type", "port", "status", "delay", "success", "total", "last_fail")
        self.tree = ttk.Treeview(result_frame, columns=columns, show="headings", height=15)
        
        # 配置列
        col_config = [
            ("目标", 150), ("类型", 80), ("端口", 60), 
            ("状态", 90), ("延迟(ms)", 100), ("成功", 80), 
            ("总数", 80), ("最后失败", 150)
        ]
        
        for col, (text, width) in zip(columns, col_config):
            self.tree.heading(col, text=text)
            self.tree.column(col, width=width, anchor="center")
        
        # 滚动条
        yscroll = ttk.Scrollbar(result_frame, orient="vertical", command=self.tree.yview)
        xscroll = ttk.Scrollbar(result_frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=yscroll.set, xscrollcommand=xscroll.set)
        
        # 布局
        self.tree.grid(row=0, column=0, sticky="nsew")
        yscroll.grid(row=0, column=1, sticky="ns")
        xscroll.grid(row=1, column=0, sticky="ew")
        
        # ========== 控制面板区域 ==========
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=3, column=0, pady=10, sticky="e")
        
        ttk.Label(control_frame, text="探测间隔（秒）:").pack(side="left", padx=5)
        self.interval_entry = ttk.Entry(control_frame, width=5)
        self.interval_entry.pack(side="left", padx=5)
        self.interval_entry.insert(0, "1")
        
        self.start_btn = ttk.Button(control_frame, text="开始检测", command=self.start_detection, width=12)
        self.start_btn.pack(side="left", padx=5)
        ttk.Button(control_frame, text="停止检测", command=self.stop_detection, width=12).pack(side="left", padx=5)
        
        # 配置颜色标签
        self.tree.tag_configure('failure', foreground='#778899')
        self.tree.tag_configure('delay_green', foreground='#2ECC40')
        self.tree.tag_configure('delay_orange', foreground='#FF851B')
        self.tree.tag_configure('delay_red', foreground='#FF4136')


    def import_targets(self):
        """使用csv模块导入CSV文件"""
        self.logger.info("开始CSV数据导入")
        
        # 选择CSV文件
        filepath = filedialog.askopenfilename(
            title="选择CSV文件",
            filetypes=[("CSV文件", "*.csv")]
        )
        if not filepath:
            return

        error_log = []
        success_count = 0
        total_rows = 0

        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                reader = csv.DictReader(f)
                
                # 验证必须包含的列
                required_columns = {'target', 'type', 'port'}
                if missing := required_columns - set(reader.fieldnames):
                    error_msg = f"CSV文件缺少必要列: {', '.join(missing)}"
                    self.logger.error(error_msg)
                    raise ValueError(error_msg)

                # 逐行处理
                for line_num, row in enumerate(reader, 2):  # 从第2行开始计数
                    total_rows += 1
                    raw_data = {
                        'target': row['target'].strip(),
                        'type': row['type'].strip().upper(),
                        'port': row['port'].strip()
                    }

                    try:
                        # === 数据验证 ===
                        # 1. 目标地址必填
                        if not raw_data['target']:
                            raise ValueError("目标地址不能为空")

                        # 2. 验证检测类型
                        if raw_data['type'] not in ('PING', 'TCP', 'UDP'):
                            raise ValueError("检测类型必须是 PING/TCP/UDP")

                        # 3. 处理端口
                        port = 0
                        if raw_data['type'] != 'PING':
                            if not raw_data['port']:
                                raise ValueError("TCP/UDP检测必须提供端口号")

                            # 处理可能的浮点格式 (如 80.0)
                            port_str = raw_data['port'].split('.')[0]
                            if not port_str.isdigit():
                                raise ValueError("端口必须是整数")

                            port = int(port_str)
                            if not (1 <= port <= 65535):
                                raise ValueError("端口范围应为1-65535")

                        # === 添加目标 ===
                        key = (raw_data['target'], raw_data['type'], str(port))
                        if key not in self.results:
                            self.targets.append({
                                "target": raw_data['target'],
                                "type": raw_data['type'],
                                "port": port
                            })
                            self.results[key] = {
                                "success": 0, 
                                "total": 0, 
                                "delay": 0,
                                "last_fail": None
                            }
                            # 插入Treeview
                            self.tree.insert("", "end", values=(
                                raw_data['target'],
                                raw_data['type'],
                                port if port != 0 else "-",
                                "待检测", "0", "0", "0", "无"
                            ))
                            success_count += 1
                            self.logger.info(f"成功添加: {key}")

                    except Exception as e:
                        error_msg = f"第{line_num}行错误: {str(e)} | 原始数据: {raw_data}"
                        error_log.append(error_msg)
                        self.logger.error(error_msg)

            # 记录错误日志
            if error_log:
                error_header = f"\n[{datetime.now()}] 文件: {os.path.basename(filepath)}\n"
                with open(IMPORT_ERROR_LOG, 'a', encoding='utf-8') as f:
                    f.write(error_header + "\n".join(error_log) + "\n")

            # 显示导入结果
            result_msg = [
                f"成功导入: {success_count}/{total_rows}",
                f"失败记录: {len(error_log)}" if error_log else ""
            ]
            messagebox.showinfo("导入结果", "\n".join(filter(None, result_msg)))

        except UnicodeDecodeError:
            messagebox.showerror("编码错误", "请确保文件使用UTF-8编码")
        except Exception as e:
            messagebox.showerror("导入失败", f"无法读取文件: {str(e)}")

    def update_port_state(self, event=None):
        if self.type_var.get() == "PING":
            self.port_entry.config(state="disabled")
        else:
            self.port_entry.config(state="normal")

    def add_target(self):
        target = self.target_entry.get().strip()
        type_ = self.type_var.get()
        port = self.port_entry.get().strip() if type_ != "PING" else "0"
        
        if not target:
            self.logger.error(f"缺少输入目标主机")
            messagebox.showerror("错误", "请输入目标主机")
            return
        
        if type_ != "PING" and (not port.isdigit() or not 0 < int(port) <= 65535):
            self.logger.error(f"输入无效端口号(1-65535)")
            messagebox.showerror("错误", "请输入有效端口号(1-65535)")
            return
        
        key = (target, type_, port)
        if key not in self.results:
            self.targets.append({
                "target": target,
                "type": type_,
                "port": int(port) if type_ != "PING" else 0
            })
            self.results[key] = {"success": 0, "total": 0, "delay": 0}
            self.tree.insert("", "end", values=(
                target, type_, port if port != "0" else "-", 
                "待检测", "0", "0", "0"
            ))
        else:
            messagebox.showwarning(f"检测目标{key}已存在!")
    def delete_selected_targets(self):
        """删除选中的检测目标"""
        selected_items = self.tree.selection()
        if not selected_items:
            messagebox.showinfo("提示", "请先选择要删除的目标")
            return
        
        # 获取待删除的keys
        delete_keys = []
        for item in selected_items:
            values = self.tree.item(item)["values"]
            target = values[0]
            type_ = values[1]
            port = str(values[2]) if values[2] != "-" else "0"
            delete_keys.append( (target, type_, port) )


        # 更新数据存储
        with self.lock:
            # 删除目标列表中的对应项
            self.targets = [
                t for t in self.targets 
                if (t["target"], t["type"], str(t["port"])) not in delete_keys
            ]
            # 删除结果字典中的对应项
            for key in delete_keys:
                if key in self.results:
                    del self.results[key]

        # 逆向删除避免索引问题
        for item in reversed(selected_items):
            self.tree.delete(item)
        
        self.logger.debug(f"已删除 {delete_keys}")
        self.logger.info(f"已删除 {len(delete_keys)} 个检测目标")

    def delete_all_targets(self):
        """清空所有检测目标"""
        if not messagebox.askyesno("确认", "确定要清空所有检测目标吗？"):
            return

        # 清空树状图
        for item in self.tree.get_children():
            self.tree.delete(item)

        # 清空数据存储
        with self.lock:
            self.targets.clear()
            self.results.clear()
        
        self.logger.info("已清空所有检测目标")

    def start_detection(self):
        if not self.targets:
            messagebox.showwarning("警告", "请先添加检测目标")
            return
        
        try:
            interval = float(self.interval_entry.get())
            self.logger.debug(f"检测间隔设置为: {interval}秒")
            if interval <= 0:
                raise ValueError
        except ValueError:
            self.logger.error(f"无效的间隔时间: {self.interval_entry.get()}")
            messagebox.showerror("错误", "请输入有效的间隔时间（大于0的数字）")
            return
        
        self.stop_event.clear()
        self.start_btn.config(state="disabled")
        threading.Thread(target=self.detection_loop, args=(interval,), daemon=True).start()

    def stop_detection(self):
        self.logger.info("停止检测")
        self.stop_event.set()
        self.start_btn.config(state="normal")

    def detection_loop(self, interval):
        while not self.stop_event.is_set():
            targets_copy = self.targets.copy() 
            for target_info in targets_copy:
                if self.stop_event.is_set():
                    return
                
                result = self.probe_target(target_info)
                self.queue.put(result)
            
            time.sleep(interval)


    def probe_target(self, target_info):
        target = target_info["target"]
        type_ = target_info["type"].upper() 
        port = target_info["port"]
        key = (target, type_, str(port))
        self.logger.debug(f"开始探测目标: {target_info}")
        try:
            if type_ == "PING":
                success, delay = self.PING_test(target)
            elif type_ == "TCP":
                success, delay = self.tcp_test(target, port)
            elif type_ == "UDP":
                success, delay = self.udp_test(target, port)
            else:
                return key, False, 0
            
        except Exception as e:
            self.logger.exception(f"探测过程中发生异常 {e}")
            return key, False, 0
        
        return key, success, delay

    def PING_test(self, host):
        """
        使用PING3库实现的PING测试
        返回: (是否成功, 延迟ms)
        """
        try:
            # 参数说明：
            # timeout: 超时时间（秒）
            # unit: 返回时间的单位（'ms' 毫秒）
            self.logger.debug(f"PING {host}")
            delay = ping(host, timeout=1, unit='ms')
            if delay and delay > 60:
                self.logger.warning(f"PING {host} delay {delay} ms")
            if delay is not None and delay > 0:
                return True, round(delay, 2)
            return False, 0
        except Exception as e:
            self.logger.error(f"{host} PING测试发生异常: {str(e)}")
            return False, 0
    def tcp_test(self, host, port):
        try:
            self.logger.debug(f"TCP {host}:{port}")
            start = time.time()
            self.logger.debug(f": {host}:{port} TCP测试开始 time:{start}")
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(1)
                s.connect((host, port))
            delay = (time.time() - start) * 1000
            self.logger.debug(f": {host}:{port} TCP测试结束 time:{time.time()} delay:{delay}")
            return True, delay
        except Exception as e:
            self.logger.error(f"tcp_test error: {str(e)}")
            return False, 0

    def udp_test(self, host, port):
        try:
            start = time.time()
            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
                s.settimeout(1)
                s.sendto(b"", (host, port))
                s.recvfrom(1024)
            delay = (time.time() - start) * 1000
            return True, delay
        except socket.timeout:
            return False, 0
        except Exception:
            return False, 0

    def process_queue(self):
        while not self.queue.empty():
            try:
                key, success, delay = self.queue.get_nowait()
                target, type_, port = key
                port_display = port if port != "0" else "-"
                result = self.results[key]
                result["total"] += 1
                if success:
                    result["success"] += 1
                    result["delay"] = delay
                    result["last_fail"] = "无"
                else:
                    result["last_fail"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    self.logger.debug(f'{target}-失败时间：{result["last_fail"]}')
                status = "成功" if success else "失败"

                tags = []
                if not success:
                    tags.append("failure")
                else:
                    # 延迟颜色分级
                    if delay > 60:
                        tags.append('delay_red')
                    elif delay > 30:
                        tags.append('delay_orange')
                    else:
                        tags.append('delay_green')

                self.logger.debug(f"color tags: {tags}")


                # ====== 新增CSV写入逻辑 ======
                csv_data = {
                    'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
                    'target': target,
                    'type': type_,
                    'port': port_display,
                    'status': "SUCCESS" if success else "FAILURE",
                    'delay_ms': f"{delay:.2f}" if success else "N/A",
                    'success_count': self.results[key]["success"],
                    'total_count': self.results[key]["total"],
                    'last_failure_time': self.results[key].get("last_fail", "N/A")
                }

                # 在独立线程中写入CSV以避免阻塞UI
                threading.Thread(
                    target=self.write_to_csv,
                    args=(csv_data,),
                    daemon=True
                ).start()

                # 更新treeview
                for item in self.tree.get_children():
                    values = self.tree.item(item)["values"]
                    current_key = (values[0], values[1], str(values[2]) if values[2] != "-" else "0")
                    if current_key == key:
                        last_fail = result.get("last_fail", "无")
                        self.logger.debug(f"+++++++++++++++++last_fail: {last_fail} ++++++++++++++")
                        self.logger.debug(f"updating {current_key},tags: {tags}")
                        self.tree.item(item, values=(
                            key[0], key[1], key[2] if key[2] != "0" else "-",
                            status,
                            f"{delay:.2f}" if success else "0",
                            result["success"],
                            result["total"],
                            last_fail
                        ),tags=tags)
                        break
            except queue.Empty:
                break
            finally:
                self.queue.task_done()
        
        self.root.after(1000, self.process_queue)

    def init_csv_file(self):
        """初始化CSV文件并写入表头"""
        csv_dir = os.path.join(LOG_ROOT, "csv_results")
        os.makedirs(csv_dir, exist_ok=True)
        
        self.csv_path = os.path.join(csv_dir, f'{datetime.now().strftime("%d_%H-%M_%S")}.csv')
        if not os.path.exists(self.csv_path):
            with open(self.csv_path, 'w', newline='', encoding='utf-8') as f:
                writer = csv.writer(f)
                writer.writerow([
                    "timestamp", "target", "type", "port",
                    "status", "delay_ms", "success_count", 
                    "total_count", "last_failure_time"
                ])
        self.logger.info(f"CSV文件已初始化创建 {self.csv_path}")        

    def write_to_csv(self, data):
        """线程安全的CSV写入方法"""
        with self.csv_lock:
            try:
                with open(self.csv_path, 'a', newline='', encoding='utf-8') as f:
                    writer = csv.writer(f)
                    writer.writerow([
                        data['timestamp'],
                        data['target'],
                        data['type'],
                        data['port'],
                        data['status'],
                        data['delay_ms'],
                        data['success_count'],
                        data['total_count'],
                        data['last_failure_time']
                    ])
                    self.logger.info(f"数据写入CSV {data['target']} {data['type']} {data['port']} {data['status']} {data['delay_ms']} {data['success_count']} {data['total_count']} {data['last_failure_time']}")
            except Exception as e:
                self.logger.error(f"CSV写入失败: {str(e)}")


    


if __name__ == "__main__":
    setup_logging()
    root = tk.Tk()
    app = NetworkMonitorApp(root)
    root.mainloop()
